﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using Effects.Effects;

namespace Effects.GUI
{
    /// <summary>
    ///  Groups instances of <see cref="Effect"/> both graphically and functionally.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   Class represents chain of Effects. Both graphically (it displays the effects) and functionally (applies this effect on input signal)
    ///   Effecting input signal is provided by <see cref="EffectBoard.Process"/> method. This method passed input signal to all of the stored effects and
    ///   return the output.
    ///  </para>
    ///  <para>
    ///   As different order of the effect results in different output, order of stored effects can be changed by MoveUp, MoveDown methods. 
    ///   EffectBords keeps one extra refeence to an effect which is currently selected. User selects it by clicking on it. All commands without arguments
    ///   are then applied to the selected effect.
    ///  </para>
    ///  <para>
    ///   <see cref="EffectBoard"/> allows other objects to monitor it's state. It raised <see cref="E:EffectBoard.StateChange"/> event whenever
    ///   an <see cref="Effect"/> is added, removed or order of effects is changed. Moreover it also raises this event to request editing of previously recorded
    ///   changes in effect's parameters. For more information see: <see cref="EffectBoardChangeType"/>
    ///  </para>
    /// </remarks>
    public partial class EffectBoard : UserControl
    {
        #region fields
        // various functional fields

        /// <summary>
        ///  Panel to put effects on.
        /// </summary>
        private Panel board;

        /// <summary>
        ///  Locker for multithread operations.
        /// </summary>
        private object locker;

        /// <summary>
        ///  Location where next addded will be placed.
        /// </summary>
        protected Point newEffect;

        /// <summary>
        ///  List of <see cref="Effect"/> currently being displayed on <see cref="EffectBoard"/>
        /// </summary>
        protected List<Effect> effectList;

        /// <summary>
        ///  Currently selected item.
        /// </summary>
        private Effect selected;

        #endregion

        #region constructor

        /// <summary>
        ///  Creates new EffectBoard.
        /// </summary>
        public EffectBoard()
        {
            InitializeComponent();

            // initialize important fields 
            locker = new object();
            this.effectList = new List<Effect>();
            this.newEffect = new Point(0, 0);

            // set initial data size
            this.dataSize = 128;
            limit = new Limiter();
            // set double-bufferd style
            this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
        }

        #endregion

        #region controls_multithread
        // wrapping method to add remove from inherited field Controls

        #region delegates

        /// <summary>
        ///  Delegate for adding <see cref="Effect"/> to the Controls
        /// </summary>
        /// <param name="effect"> Effect to add </param>
        private delegate void ControlsAddInvoker(Effect effect);

        /// <summary>
        ///  Delegate for adding array of <see cref="Effect"/> to the Controls
        /// </summary>
        /// <param name="effect"> Effects to add </param>
        private delegate void ControlsAddManyInvoker(Effect[] effect);

        /// <summary>
        ///  delegate for removing Controls at specified index.
        /// </summary>
        /// <param name="index"> Specifies Control to remove. </param>
        private delegate void ControlsRemoveAtInvoker(int index);

        /// <summary>
        ///  Delegate for removing <see cref="Effect"/> from the Controls
        /// </summary>
        /// <param name="e"> Effect to remove </param>
        private delegate void ControlsRemoveInvoker(Effect e);

        #endregion

        #region methods
        // method for adding and removing Controls from other threads

        /// <summary>
        ///  Adding to the Controls usable from all threads
        /// </summary>
        /// <param name="effect"> Effect to add </param>
        private void ControlsAddMultiThread(Effect effect)
        {
            if (InvokeRequired)
                BeginInvoke(new ControlsAddInvoker(this.board.Controls.Add), new object[] { effect });
            else
                this.board.Controls.Add(effect);
            
        }
        
        /// <summary>
        ///  Adding to the Controls usable from all threads
        /// </summary>
        /// <param name="effects"> Effects to add </param>
        private void ControlsAddMultiThread(Effect[] effects)
        {
            if (InvokeRequired)
                BeginInvoke(new ControlsAddManyInvoker(this.ControlsAddMultiThread), new object[] { effects });
            else
                this.board.Controls.AddRange(effects);
        }

        /// <summary>
        ///  Removing from Controls at specified index.
        /// </summary>
        /// <param name="index"> Specifies control to remove </param>
        private void ControlsRemoveAtMultiThread(int index)
        {
            if (InvokeRequired)
                BeginInvoke(new ControlsRemoveAtInvoker(this.board.Controls.RemoveAt), new object[] { index });
            else
                this.board.Controls.RemoveAt(index);
        }

        /// <summary>
        ///  Removing Effect from the Controls.
        /// </summary>
        /// <param name="e"> Effect to remove </param>
        private void ControlsRemoveMultiThread(Effect e)
        {
            if (InvokeRequired)
                BeginInvoke(new ControlsRemoveInvoker(this.board.Controls.Remove), new object[] { e });
            else
                this.board.Controls.Remove(e);
        }

        #endregion

        #endregion

        #region add_effect
        // adding effect to the board

        /// <summary>
        ///  Adds effect to the <see cref="EffectBoard"/>. Given <see cref="Effect"/> will be last on the board.
        /// </summary>
        /// <param name="effect"> Effect to add. </param>
        public void Add(Effect effect)
        {
            lock (locker)
            {
                // register event handler on every added effect, to select effect, when user clicks on it
                effect.Click += this.EffectSelected;

                // set important technical properties
                effect.SampleRate = this.sampleRate;
                effect.DataSize = this.dataSize;
                // set location and move prepare newEffect for next effect
                effect.Location = this.newEffect;
                this.newEffect.X = this.newEffect.X + effect.Width;

                this.effectList.Add(effect);
                ControlsAddMultiThread(effect);
                RefreshMultiThread();

                //raise event
                EffectBoardChangeEventArgs args = new EffectBoardChangeEventArgs(effectList.Count - 1, effect, EffectBoardChangeType.effectAdded);
                if (StateChange != null)
                    StateChange(this, args);
            }
        }

        #endregion

        #region remove_effect
        // removing effect from the board

        /// <summary>
        ///  Removes given <see cref="Effect"/> from the <see cref="EffectBoard"/>
        /// </summary>
        /// <param name="effect"> Effect to remove </param>
        public void Remove(Effect effect)
        {
            lock (locker)
            {
                int index = effectList.IndexOf(effect);
                RemoveAt(index);
            }
        }

        /// <summary>
        ///  Removes <see cref="Effect"/> at given index.
        /// </summary>
        /// <param name="index"> Specifies position of the <see cref="Effect"/> to remove </param>
        public void RemoveAt(int index)
        {
            lock (locker)
            {
                Effect removed = effectList[index];
                this.effectList.RemoveAt(index);
                ControlsRemoveMultiThread(removed);

                //this.SetBoardWidth();
                this.PackEffectsMultiThread(index);
                this.RefreshMultiThread();

                EffectBoardChangeEventArgs args = new EffectBoardChangeEventArgs(index, removed, EffectBoardChangeType.effectRemoved);
                if (StateChange != null)
                    StateChange(this, args);
            }
        }

        /// <summary>
        ///  Removes selected effect.
        /// </summary>
        public void RemoveSelected()
        {
            if (selected != null)
            {
                Remove(selected);
                selected = null;
            }
        }

        /// <summary>
        ///  Removes all effects.
        /// </summary>
        public void RemoveAll()
        {
            lock (locker)
            {
                foreach (Effect e in effectList)
                    board.Controls.Remove(e);

                effectList = new List<Effect>();
                newEffect = new Point(0, 0);
            }

            RefreshMultiThread();
        }

        /// <summary>
        ///  Removes all moves effect thread safe.
        /// </summary>
        public void RemoveAllMutliThread()
        {
            if (InvokeRequired)
            {
                IAsyncResult asyncResult = BeginInvoke(new MethodInvoker(RemoveAll));
                EndInvoke(asyncResult);
            }
            else
                RemoveAll();
        }

        #endregion

        #region reset
        // reseting effects

        /// <summary>
        ///  Resets all stored effects.
        /// </summary>
        public void Reset()
        {
            lock (locker)
            {
                foreach (Effect e in effectList)
                    e.Reset();
            }
        }

        #endregion

        #region refresh
        // refresh from all threads

        /// <summary>
        ///  Repaints the <see cref="EffectBoard"/> and is usable from all threads.
        /// </summary>
        private void RefreshMultiThread()
        {
            if (InvokeRequired)
                BeginInvoke(new MethodInvoker(this.Refresh));
            else
                Refresh();
        }

        #endregion

        #region pack_effects
        // vysually ordering the effects

        /// <summary>
        ///  Visually orders stored effects from given index. 
        /// </summary>
        /// <param name="index"> Index to start ordering from </param>
        /// <remarks>
        ///  <para>
        ///   Method is used to keep all effect on the <see cref="EffectBoard"/> nicely placed and aligned. It starts this reordering from given
        ///   index and continues toward the end of the board (maximum index)
        ///  </para>
        /// </remarks>
        private void PackEffects(int index)
        {
            lock (locker)
            {
                if (effectList.Count > 0)
                {
                    if (index == 0)
                    {
                        effectList[0].Location = new Point(0, 0);
                        for (int i = index + 1; i < effectList.Count; i++)
                            effectList[i].Location = new Point(effectList[i - 1].Location.X + effectList[i - 1].Width, 0);
                    }
                    else
                    {
                        for (int i = index; i < effectList.Count; i++)
                            effectList[i].Location = new Point(effectList[i - 1].Location.X + effectList[i - 1].Width, 0);
                    }
                }

                if (effectList.Count > 0)
                    newEffect = new Point(effectList[effectList.Count - 1].Location.X + effectList[effectList.Count - 1].Width, 0);
                else
                    newEffect = new Point(0, 0);
            }
        }

        /// <summary>
        ///  Delegate for calling <see cref="EffectBoard.PackEffects"/> method
        /// </summary>
        /// <param name="index"></param>
        private delegate void PackEffectsInvoker(int index);

        /// <summary>
        ///  Visually orders stored effects from given index. Usable from all thrieads
        /// </summary>
        /// <param name="index"> Index to start ordering from </param>
        private void PackEffectsMultiThread(int index)
        {
            if (InvokeRequired)
                BeginInvoke(new PackEffectsInvoker(this.PackEffects), new object[] { index });
            else
                PackEffects(index);
        }

        #endregion

        #region selected
        // selects effect

        // Event handler registered on every effect that is added to the board
        /// <summary>
        ///  Changes curently selected effect.
        /// </summary>
        /// <param name="sender"> Object that has raised the event </param>
        /// <param name="e"> Event arguments </param>
        /// <remarks>
        ///  Method is register on <see cref="E:Effect.Click"/> event of every added <see cref="Effect"/>. When this event is raised method
        ///  sets sender as selected effect.
        /// </remarks>
        public void EffectSelected(object sender, EventArgs e)
        {
            lock (locker)
            {
                if (selected != null)
                    selected.Selected = false;
                selected = (Effect)sender;
                selected.Selected = true;
            }
        }

        #endregion

        #region move_up
        //moving effect up (left)

        /// <summary>
        ///  Moves up selected effect one postition.
        /// </summary>
        public void MoveUp()
        {
            if (selected != null)
                MoveUp(selected);
        }

        /// <summary>
        ///  Moves up given <see cref="Effect"/> one position.
        /// </summary>
        /// <param name="effect"> Effect to move up </param>
        private void MoveUp(Effect effect)
        {
            lock (locker)
            {
                int index = effectList.IndexOf(effect);
                MoveUp(index);
            }
        }

        /// <summary>
        ///  Moves up effect at given index.
        /// </summary>
        /// <param name="index"> Index of the effect to move uo. </param>
        public void MoveUp(int index)
        {
            lock (locker)
            {
                if (index > 0 && index < effectList.Count)
                {
                    SwapEffectsMultiThread(index, index - 1);
                    RefreshMultiThread();

                    EffectBoardChangeEventArgs args = new EffectBoardChangeEventArgs(index - 1, effectList[index], EffectBoardChangeType.effectMoveDown);
                    if (StateChange != null)
                        StateChange(this, args);
                }
            }
        }

        #endregion

        #region move_down
        // move effect down (right)

        /// <summary>
        ///  Moves selected effect down one position.
        /// </summary>
        public void MoveDown()
        {
            if (selected != null)
                MoveDown(selected);
        }

        /// <summary>
        ///  Moves down given <see cref="Effect"/> one position.
        /// </summary>
        /// <param name="effect"> Effect to move up </param>
        private void MoveDown(Effect effect)
        {
            lock (locker)
            {
                int index = effectList.IndexOf(effect);
                MoveDown(index);
            }
        }

        /// <summary>
        ///  Moves up effect at given index.
        /// </summary>
        /// <param name="index"> Index of the effect to move uo. </param>
        public void MoveDown(int index)
        {
            lock (locker)
            {
                if (index > -1 && index < effectList.Count - 1)
                {
                    SwapEffectsMultiThread(index, index + 1);
                    RefreshMultiThread();

                    EffectBoardChangeEventArgs args = new EffectBoardChangeEventArgs(index, effectList[index + 1], EffectBoardChangeType.effectMoveDown);
                    if (StateChange != null)
                        StateChange(this, args);
                }
            }
        }
        #endregion

        #region edit_changes

        /// <summary>
        ///  Raises the event that class that recording changes can allow user editing them
        /// </summary>
        public void EditChanges()
        {
            if (selected != null && StateChange != null)
            {
                EffectBoardChangeEventArgs args = new EffectBoardChangeEventArgs(0, selected, EffectBoardChangeType.editChanges);
                this.StateChange(this, args);
            }
        }

        #endregion

        #region swapping_2_effect

        /// <summary>
        ///  Invokes <see cref="EffectBoard.SwapEffects"/> method.
        /// </summary>
        /// <param name="e1"> Index of effect 1 to swap. </param>
        /// <param name="e2"> Index of effect 2 to swap. </param>
        private delegate void SwapEffectsInvoker(int e1, int e2);

        /// <summary>
        ///  Swaps given effects on the board. Callable from all threads.
        /// </summary>
        /// <param name="index1"> Index of effect 1 to swap. </param>
        /// <param name="index2"> Index of effect 2 to swap. </param>
        private void SwapEffectsMultiThread(int index1, int index2)
        {
            if (InvokeRequired)
                BeginInvoke(new SwapEffectsInvoker(this.SwapEffects), new object[] { index1, index2 });
            else
                SwapEffects(index1, index2);
        }

        /// <summary>
        ///  Swaps given effects on the board.
        /// </summary>
        /// <param name="index1"> Index of effect 1 to swap. </param>
        /// <param name="index2"> Index of effect 2 to swap. </param>
        private void SwapEffects(int index1,int index2)
        {
            lock (locker)
            {
                Effect pom = effectList[index1];
                System.Drawing.Point pomLocation = pom.Location;

                pom.Location = effectList[index2].Location;
                effectList[index2].Location = pomLocation;

                effectList[index1] = effectList[index2];
                effectList[index2] = pom;
                PackEffects(Math.Min(index1, index2));
            }
        }
        #endregion


        /// <summary>
        ///  Raised when any change to the board occures.
        /// </summary>
        public event EffectBoardChangeEventHandler StateChange;

        #region process
        // process input signal

        /// <summary>
        ///  Apply effect on given input signal and return output signal.
        /// </summary>
        /// <param name="data"> Input signal </param>
        /// <returns> Output signal after applying effects </returns>
        /// <remarks>
        ///  <para>
        ///   Methods expect that given array containing input signal will be of length previously set by calling <see cref="EffectBoard.SetDataSize"/> method.
        ///   Otherwise it may cause unwanted results.
        ///  </para>
        /// </remarks>
        public double[] Process(double[] data)
        {
            double[] buffer = data;
            lock (locker)
            {
                foreach (Effect e in effectList)
                {
                    buffer = e.effect(buffer);
                }

                buffer = limit.effect(buffer);
            }
            return buffer;
        }


        /// <summary>
        ///  Limiter used to controls the signal strenght.
        /// </summary>
        private Limiter limit;

        #endregion

        #region Get_Set_effects

        /// <summary>
        ///  Returns array of names desribing all curently stored effects.
        /// </summary>
        /// <returns></returns>
        public string[] GetEffectTypes()
        {
            lock (locker)
            {
                string[] types = new string[effectList.Count];
                for (int i = 0; i < effectList.Count; i++)
                    types[i] = effectList[i].Name;
                return types;
            }
        }

        /// <summary>
        ///  Returns array containing all curently stored effects.
        /// </summary>
        /// <returns></returns>
        public Effect[] GetAllEffects()
        {
            lock (locker)
            {
                return effectList.ToArray<Effect>();
            }
        }

        /// <summary>
        ///  Stores given effects and removes all effects stored previously.
        /// </summary>
        /// <param name="effects"> Effects to store. </param>
        public void SetEffects(Effect[] effects)
        {
            // prepare locations for effects
            Point upperLeft = new Point(0, 0);
            int totalWidth = 0;
            foreach (Effect e in effects)
            {
                //e.SettingsChanged += this.EffectSettingsChange;
                e.Click += this.EffectSelected;
                e.Location = upperLeft;
                upperLeft = new Point(upperLeft.X + e.Width, upperLeft.Y);
                totalWidth = e.Width;
            }

            // remove old effects and add new
            lock (locker)
            {
                RemoveAllMutliThread();

                ControlsAddMultiThread(effects);
                effectList = effects.ToList<Effect>();
                SetSampleRate(this.sampleRate);
                SetDataSize(this.dataSize);
                newEffect = new Point(totalWidth, 0);
            }
        }

        #endregion

        #region setting_tech_properties
        // Setting technical properties - data size, sampling rate

        /// <summary>
        ///  Current dataSize.
        /// </summary>
        private int dataSize;
        
        /// <summary>
        ///  Sets dataSize to all stored Effects
        /// </summary>
        /// <param name="dataSize"> Desired data size </param>
        /// <remarks>
        ///  Data size defines length of the array passed to the <see cref="EffectBoard.Process"/> method.
        /// </remarks>
        public void SetDataSize(int dataSize)
        {
            lock (locker)
            {
                this.dataSize = dataSize;
                foreach (Effect e in effectList)
                    e.DataSize = dataSize;
            }
        }

        /// <summary>
        ///  Current sampling rate.
        /// </summary>
        private uint sampleRate;

        /// <summary>
        ///  Sets sampling rate to all stored Effects.
        /// </summary>
        /// <param name="sampleRate"> Desired sampling rate </param>
        public void SetSampleRate(uint sampleRate)
        {
            lock (locker)
            {
                this.sampleRate = sampleRate;
                foreach (Effect e in effectList)
                    e.SampleRate = sampleRate;
            }
        }

        #endregion
    }
}
